import os
import re

from lxml import etree
from ruamel import yaml

dollar_pattern = r"\$\{(.*?)\}"


class MavenDepends(object):
    def __init__(self):
        self.default_dependency = set()
        self.profile_map = {}
        self.plugin_map = {}
        self.var_map = {}
        self.package_names = set()
        self.ns = {'ns': 'http://maven.apache.org/POM/4.0.0'}
        self.spec_map = {'@groovyGroupId@':'org.codehaus.groovy'}
    @staticmethod
    def get_tag(group_id, artifact_id, version, scope):
        if group_id is None:
            return artifact_id
        return str(group_id) + ':' + str(artifact_id)

    def parse_dir(self, path):
        #first the main pom.xml
        main_pom = os.path.join(path, "pom.xml")
        if not os.path.exists(main_pom):
            print("no 'pom.xml' found at home!")
            return
        self.parse(main_pom)
        for dirpath, dirnames, filenames in os.walk(path):
            for filename in filenames:
                if filename == 'pom.xml':
                    file_path = os.path.join(dirpath, filename)
                    if file_path != main_pom:
                        self.parse(file_path)


    def parse(self, path):
        tree = etree.parse(path)
        root = tree.getroot()
        # root.iter()
        default_package = root.find('./ns:groupId', self.ns)
        if default_package is not None:
            pkg_text = self.replace_vars(default_package.text)
            self.var_map['groupId'] = pkg_text
            self.var_map['project.groupId'] = pkg_text
            self.var_map['project.parent.groupId'] = pkg_text
            self.spec_map['@project.groupId@'] = pkg_text

            default_artifact = root.find('./ns:artifactId', self.ns)
            art_text = self.replace_vars(default_artifact.text)

            self.var_map['artifactId'] = art_text
            self.spec_map['@project.artifactId@'] = art_text


            default_package_name = self.get_tag(pkg_text, art_text, None, None)
            self.package_names.add(default_package_name)
            self.default_dependency.add(default_package_name + ':pom:')
            self.default_dependency.add(default_package_name)
        parent_package = root.find('./ns:parent/ns:groupId', self.ns)
        if parent_package is not None:
            pkg_text = self.replace_vars(parent_package.text)
            self.var_map['groupId'] = pkg_text
            self.var_map['project.groupId'] = pkg_text
            self.var_map['project.parent.groupId'] = pkg_text
            self.spec_map['@project.groupId@'] = pkg_text

            parent_artifact = root.find('./ns:parent/ns:artifactId', self.ns)
            art_text = self.replace_vars(parent_artifact.text)
            self.var_map['artifactId'] = art_text
            self.spec_map['@project.artifactId@'] = art_text

            parent_package_name = self.get_tag(pkg_text, art_text, None, None)
            self.package_names.add(parent_package_name)
            self.default_dependency.add(parent_package_name)
            self.default_dependency.add(parent_package_name + ':pom:')

        self.var_map = self.parse_properities(tree, self.ns)


        default_dependencies = root.find('./ns:dependencies', self.ns)
        if default_dependencies is not None:
            self.default_dependency = self.parse_dependencies(default_dependencies, self.default_dependency)
        another_default_dependencies = root.find('./ns:dependencyManagement/ns:dependencies', self.ns)
        if another_default_dependencies is not None:
            self.default_dependency = self.parse_dependencies(another_default_dependencies, self.default_dependency)


        # plugins first, as profiles may depend on plugins
        plugins = tree.find('./ns:build/ns:plugins', self.ns)
        if plugins is not None:
            self.parse_plugins(plugins, self.plugin_map)
        another_plugins = tree.find('./ns:build/ns:pluginManagement/ns:plugins', self.ns)
        if another_plugins is not None:
            self.parse_plugins(another_plugins, self.plugin_map)

        profiles = tree.find('./ns:profiles', self.ns)
        if profiles is not None:
            self.parse_profiles(profiles, self.profile_map)
        ## at least, fill all the plugins to the default_dependency
        for plugin_id in self.plugin_map:
            self.default_dependency.add(plugin_id)
            plugin_dependencies = self.plugin_map[plugin_id]
            for plugin_dependency in plugin_dependencies:
                self.default_dependency.add(plugin_dependency)
        self.parse_all_other_deps(tree, self.ns)

    def parse_properities(self, tree, ns):
        for mvn_property in tree.findall('ns:properties', ns):
            children = mvn_property.getchildren()
            for child in children:
                tag = child.tag
                if isinstance(tag, str):
                    tag = tag.replace('{http://maven.apache.org/POM/4.0.0}', '')
                    text = self.replace_vars(child.text)
                    self.var_map[tag] = text
        return self.var_map

    def parse_all_other_deps(self, tree, ns):
        for mvn_executable in tree.findall('.//ns:executable', ns):
            text = mvn_executable.text
            if isinstance(text, str):
                text = text.replace('{http://maven.apache.org/POM/4.0.0}', '')
                self.default_dependency.add(text)


    def replace_vars(self, given_str):
        if given_str is None:
            return ''
        if '${' not in given_str and '@' not in given_str:
            return given_str
        return_str = given_str
        if '${' in return_str:
            matches = re.findall(dollar_pattern, return_str)
            if len(matches) > 0:
                for match in matches:
                    if match in self.var_map:
                        return_str = return_str.replace('${' + match + '}', self.var_map[match])
        if '@' in return_str:
            for key in self.spec_map:
                if key in return_str:
                    return_str = return_str.replace(key, self.spec_map[key])
        return return_str

    def parse_profiles(self, profiles, given_map):
        for profile in profiles.findall('./ns:profile', self.ns):
            profile_id_ins = profile.find('./ns:id', self.ns)
            if profile_id_ins is not None:
                profile_id = profile.find('./ns:id', self.ns).text
            else:
                profile_id = ''
            dependencies = profile.find('./ns:dependencies', self.ns)
            dependency_text_set = set()
            if dependencies is not None:
                dependency_text_set = self.parse_dependencies(dependencies, dependency_text_set)
            # parse the plugins
            inner_plugin = profile.find('./ns:build/ns:plugins', self.ns)
            if inner_plugin is not None:
                inner_plugin_map = self.parse_plugins(inner_plugin, {})
                for plugin_id in inner_plugin_map:
                    dependency_text_set.add(plugin_id)
                    plugin_dependencies = inner_plugin_map[plugin_id]
                    dependency_text_set = dependency_text_set | plugin_dependencies
                    # load the plugin map again
                    if plugin_id in self.plugin_map:
                        dependency_text_set = dependency_text_set | self.plugin_map[plugin_id]
            if profile_id not in given_map:
                given_map[profile_id] = dependency_text_set
            else:
                given_map[profile_id] = given_map[profile_id] | dependency_text_set
        return given_map

    def parse_plugins(self, plugins, given_map):
        plugin_set = plugins.findall('./ns:plugin', self.ns)
        for plugin in plugin_set:
            tag = self.parse_single_dependency_or_plugin(plugin)
            dependencies = plugin.find('./ns:dependencies', self.ns)
            dependency_text_set = set()
            if dependencies is not None:
                dependency_text_set = self.parse_dependencies(dependencies, dependency_text_set)
            if tag not in given_map:
                given_map[tag] = dependency_text_set
            else:
                given_map[tag] = given_map[tag] | dependency_text_set
        return given_map

    # input: dependencies
    def parse_dependencies(self, dependencies, dependency_text_set):
        for dependency in dependencies.findall('./ns:dependency', self.ns):
            tag = self.parse_single_dependency_or_plugin(dependency)
            # if self.package_names not in tag:
            dependency_text_set.add(tag)
        return dependency_text_set

    # input: dependency
    def parse_single_dependency_or_plugin(self, dependency):
        get_text = lambda tag: dependency.find(f'./ns:{tag}', self.ns).text if dependency.find(f'./ns:{tag}', self.ns) is not None else None
        group_id = self.replace_vars(get_text('groupId'))
        artifact_id = self.replace_vars(get_text('artifactId'))
        version = self.replace_vars(get_text('version'))
        scope = self.replace_vars(get_text('scope'))
        return self.get_tag(group_id, artifact_id, version, scope)



    def save_maven(self, dir, depends_file_name):
        # self.set_style(self.libraries_dict)
        yml = yaml.YAML()
        # yml.indent(offset=4)
        yml.width = 4096
        option_dict = {}
        for key in self.profile_map:
            option_dict[key] = {"name": key}
        option_yml = os.path.join(dir, "options.yaml")
        with open(option_yml, 'w') as file:
            yml.dump(option_dict, file)

        option_dep_map = {}
        fn = os.path.join(dir, depends_file_name)
        default_option = "buildRequires"
        option_dep_map[default_option] = list(self.default_dependency)
        for option in self.profile_map:
            new_option = f"buildRequires when -{option}"
            option_dep_map[new_option] = list(self.profile_map[option])
        with open(fn, 'w') as file:
            yml.dump(option_dep_map, file)

def get_direct_children(directory):
    files = []
    folders = []

    with os.scandir(directory) as entries:
        for entry in entries:
            if entry.is_file():
                files.append(entry.name)
            elif entry.is_dir():
                folders.append(entry.name)

    return files, folders

if __name__ == '__main__':
    # for single
    # maven = MavenDepends()
    # given_path = os.path.join(".", "package-mapping-results", "maven_raw_testdata", "narayana", "narayana-5.3.3.Final")
    # maven.parse_dir(given_path)
    # maven.save_maven(given_path, "depends.yaml")
    # for group
    given_path = 'E:\maven_raw_test_single'
    given_files, give_folders = get_direct_children(given_path)
    total_size = len(give_folders)
    current_size = 1
    for project in give_folders:
        project_path = os.path.join(given_path, project)
        inner_given_files, inner_give_folders = get_direct_children(project_path)
        real_project_name = ''
        if len(inner_give_folders) >= 1:
            real_project_name = inner_give_folders[0]
            real_project_path = os.path.join(project_path, real_project_name)
            maven = MavenDepends()
            maven.parse_dir(real_project_path)
            maven.save_maven(real_project_path, "depends.yaml")
        else:
            print('-----no src files found-----')
        print("----processing file:" + real_project_name + f":{current_size}/{total_size}----")
        current_size += 1

    print('-----over-----')
